package pers.mihao.quickstream.common.plus.jdbc.blodfiled.mybatis;


import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.ParameterizedTypeImpl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.TableId;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import pers.mihao.quickstream.common.exception.BaseException;
import pers.mihao.quickstream.common.plus.jdbc.blodfiled.BlobField;
import pers.mihao.quickstream.common.util.CollectionUtil;
import pers.mihao.quickstream.common.util.ReflectionUtil;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.List;
import java.util.Properties;

import static pers.mihao.quickstream.common.util.ReflectionUtil.*;

/**
 * blod序列化
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}),
})
public class BlobFiledInterceptor implements Interceptor {

    @Resource
    DataSource dataSource;

    private final String blobFiledName = "blob_data";
    private final String blobTableName = "blob_save";

    @Override
    public Object intercept(Invocation invoke) throws Throwable {
        if (invoke.getTarget() instanceof ResultSetHandler) {
            return wrapperBlobValue(invoke.proceed());
        } else if (invoke.getTarget() instanceof Executor) {
            if (invoke.getArgs() != null) {
                for (Object obj : invoke.getArgs()) {
                    if (obj instanceof MappedStatement) {
                        continue;
                    }
                    List<Field> fields;
                    if (hasAnnFiled(obj.getClass(), TableId.class)
                            && !(fields = getAnnFiledList(obj.getClass(), BlobField.class)).isEmpty()) {
                        writeDataToBlob(fields, obj);
                    }
                }
            }
        }
        return invoke.proceed();
    }

    private void writeDataToBlob(List<Field> fields, Object obj) {
        for (Field field : fields) {
            Object saveData = getValueByField(obj, field);
            if (saveData != null) {
                String bizId = getBizId(obj);
                doSaveOrUpdate(bizId, obj.getClass().getName(), field.getName(), saveData, field);
            }
        }
    }

    private void doSaveOrUpdate(String bizId, String bizType, String typeName, Object saveData, Field field) {
        String updateSql = null;
        if (hasFieldValue(bizId, bizType, typeName)) {
            updateSql = getUpdateSql();
        } else {
            updateSql = getInsertSql();
        }
        try (Connection connection = getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(updateSql)) {
            byte[] data = serialize(field, saveData);
            InputStream inputStream = new ByteArrayInputStream(data);
            preparedStatement.setBlob(1, inputStream);
            preparedStatement.setString(2, bizId);
            preparedStatement.setString(3, bizType);
            preparedStatement.setString(4, typeName);
            int updateRes = preparedStatement.executeUpdate();
            if (updateRes != 1) {
                throw new BaseException("update error sql :" + updateSql);
            }
        } catch (SQLException e) {
            throw new BaseException(e);
        }
    }



    protected String getUpdateSql() {
        return "update " + blobTableName +
                " set " + blobFiledName + " = ?, " +
                "biz_id" + " = ? where " +
                "biz_type" + " = ? and " +
                "type_filed" + " = ?";
    }

    protected String getInsertSql() {
        return "insert into " + blobTableName + "(" +
                blobFiledName + "," +
                "biz_id" + "," +
                "biz_type" + "," +
                "type_filed" + ") values(?,?,?,?);";
    }

    private Object wrapperBlobValue(Object proceed) {
        /**
         * fixme 存在循环栈
         */
        if (proceed instanceof List) {
            List<?> objList = (List<?>) proceed;
            if (CollectionUtil.isNotEmpty(objList)) {
                for (Object obj : objList) {
                    wrapperBlobValue(obj);
                }
            }
        } else {
            List<Field> fields = getAnnFiledList(proceed.getClass(), BlobField.class);
            if (CollectionUtil.isNotEmpty(fields)) {
                String bizId = getBizId(proceed);
                for (Field field : fields) {
                    setFieldValue(field, bizId, proceed);
                }
            }
        }
        return proceed;
    }

    private String getBizId(Object proceed) {
        List<Field> tableId = getAnnFiledList(proceed.getClass(), TableId.class);
        StringBuilder bizId = new StringBuilder();
        for (Field id : tableId) {
            bizId.append(getValueByField(proceed, id));
        }
        return bizId.toString();
    }


    private byte[] getFieldValue(String bizId, String bizType, String typeName) {
        String selectSql = getSelectSql(blobFiledName);
        try (Connection connection = getConnection();
             PreparedStatement preparedStatement = connection.prepareStatement(selectSql)) {
            preparedStatement.setString(1, bizId);
            preparedStatement.setString(2, bizType);
            preparedStatement.setString(3, typeName);
            ResultSet resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                Blob blob = resultSet.getBlob(blobFiledName);
                if (blob != null) {
                    return IoUtil.readBytes(blob.getBinaryStream());
                }
            }
        } catch (SQLException e) {
            throw new BaseException(e);
        }
        return null;
    }

    private boolean hasFieldValue(String bizId, String bizType, String typeName) {
        String blobTableId = "id";
        String selectSql = getSelectSql(blobTableId);
        try (Connection connection = getConnection();
             PreparedStatement preparedStatement = connection.prepareStatement(selectSql)) {
            preparedStatement.setString(1, bizId);
            preparedStatement.setString(2, bizType);
            preparedStatement.setString(3, typeName);
            ResultSet resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                return true;
            }
        } catch (SQLException e) {
            throw new BaseException(e);
        }
        return false;
    }

    private void setFieldValue(Field field, String id, Object obj) {
        byte[] blobByte = getFieldValue(id, obj.getClass().getName(), field.getName());
        if (blobByte == null) {
            return;
        }
        Object value = unSerialize(field, blobByte);
        ReflectionUtil.setValueByFieldName(obj, field.getName(), value);
    }

    private String getSelectSql(String key) {
        return "select " + key +
                " from " + blobTableName +
                " where biz_id" + " = ?" +
                " and biz_type" + " = ?" +
                " and type_filed" + " = ?";
    }


    public Object plugin(Object arg0) {
        return Plugin.wrap(arg0, this);
    }

    private Connection getConnection() {
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 序列化
     * @param field
     * @param saveData
     * @return
     */
    private byte[] serialize(Field field, Object saveData) {
        return JSONObject.toJSONBytes(saveData);
    }

    /**
     * 反序列化
     * @param field
     * @param blobByte
     * @return
     */
    private Object unSerialize(Field field, byte[] blobByte) {
        Object value;
        if (field.getType().equals(List.class)) {
            Class<?> clazz = (Class<?>) ((ParameterizedTypeImpl) field.getGenericType()).getActualTypeArguments()[0];
            value = JSONArray.parseArray(new String(blobByte), clazz);
        } else {
            value = JSONObject.parseObject(blobByte, field.getType());
        }
        return value;
    }

}
